import java.awt.Point;
import java.util.*;

import com.thoughtworks.xstream.XStream;

class ROI
{
	// Original image file name
	private String filename;

	// Upper left corner of ROI in original image
	private int original_xpos;
	private int original_ypos;

	// ROI width, height
	private int width;
	private int height;

	private ImageData original_image;
	private ImageData binary_image;
	private ImageData labeled_image;
	private ImageData regrown_labeled_image;

	// ?? Boni - should this be a separate image?
	// For now (temporary) this image shows the colored masses.
	private ImageData visualized_image;

	private Vector<Mass> Mass_Vector;

	////////////////////
	// Methods
	

	/**
	 * Load image from file.
	 * ROI is full image.
	 */
	public ROI(String filename)
	{
		original_image = new ImageData(filename);
		original_xpos = 0;
		original_ypos = 0;
		width = original_image.getWidth();
		height = original_image.getHeight();
		binary_image = null;
		labeled_image = null;
		regrown_labeled_image = null;
		
		Mass_Vector = new Vector<Mass>();
//		test_init();
	}

	private void test_init()
	{
		for(int i = 1; i<=2; i++) {
			Mass_Vector.add(new Mass(this, i));
		}
		doThreshold(127);
		doMassDetect();
		doRegionGrowing();
		doFindLabeledMasses();
	}	

	/**
	 * Performs thresholding using the specified tvalue.
	 * Thresholding is performed on the original image;
	 * results stored in the binary image.
	 * @param tvalue
	 */
	public void doThreshold(int tvalue) {
		binary_image = original_image.threshold(tvalue);		
	}
	
	/**
	 * Performs mass detection on binary image.
	 * Thresholding must be performed first.
	 * Mass detection results are stored in labeled image.
	 */
	public void doMassDetect() {
		labeled_image = massDetect(binary_image);
	}

	/**
	 * Performs region growing on labeled image.
	 * Stores results in regrown labeled image.
	 */
	public void doRegionGrowing() {
		regrown_labeled_image = regionGrowing(labeled_image);		
	}
	
	/**
	 * Searches for labeled masses on regrown labeled
	 * image and creates a mass object for each
	 * distinct label found.
	 * Once all masses are found, detectALLFeatures is called
	 * for each mass.
	 * Also normalizes features.
	 */
	public void doFindLabeledMasses() {
		findLabeledMasses();
		for(Mass mass : Mass_Vector) {
			// TODO: Fix exceptions at a lower level.  There are divide
			// by zero errors happening.
			try {
				mass.detectALLFeatures();
			} catch (Exception e) {
				p.print("An exception occured for mass: " + mass.getMassID());
				p.print(e.toString());
			}
		}
		
		// we probably only need to normalize area and thinness.
		// sharpness is already on a 0 to 1 scale.
		// grey (avg and sigma^2) don't need normalization.
		// boundary_len is not visualized.
		String[] fieldNames = {"Area", "Average_Grey", "Variance_Grey", "Boundary_Length", "Sharpness", "thinness"};
		
		for (String fieldName : fieldNames) {
			float max = Float.MIN_VALUE;
			float min = Float.MAX_VALUE;
			try {
				for(Mass mass : Mass_Vector) {
					float value = ((Feature) Mass.class.getField(fieldName).get(mass)).value;
					if(value < min) {
						min = value;
					}
					if(value > max) {
						max = value;
					}
				}
				for(Mass mass: Mass_Vector) {
					float value = ((Feature) Mass.class.getField(fieldName).get(mass)).value;
					float result = 0;
					if(Mass_Vector.size() > 1) {
						result = (value - min) / (max - min);
					}
					((Feature) Mass.class.getField(fieldName).get(mass)).normalized_value = result;
				}
			} catch (IllegalArgumentException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (SecurityException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (NoSuchFieldException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	
	/**
	 * Load image from another image.
	 * ROI is given by x, y, w, h
	 */
	public ROI(ImageData img, int x, int y, int w, int h)
	{
		original_image = new ImageData(img, x, y, w, h);
		original_xpos = x;
		original_ypos = y;
		width = w;
		height = h;

		binary_image = null;
		labeled_image = null;
		Mass_Vector = new Vector<Mass>();
		
		//test_init();
	}

	public ImageData getOriginalImage()	{ return original_image; }
	public ImageData getBinaryImage()		{ return binary_image; }
	public ImageData getLabeledImage()	{ return labeled_image; }
	public ImageData getRegrownLabeledImage()	{ return regrown_labeled_image; }
	public ImageData getVisualizedImage()	{ return visualized_image; }
	public void setVisualizedImage(ImageData img) { visualized_image = img; }

	public String toXML()
	{
		XStream xstream = new XStream();
		return xstream.toXML(this);
	}
	
	/**
	 * Adds a mass object to Mass_Vector
	 */
	public boolean addMass(Mass arg0) {
		return Mass_Vector.add(arg0);
	}

	/**
	 * Returns list of Mass_Vector Mass objects
	 */
	public Vector<Mass> getMasses()
	{
		return Mass_Vector;
	}

	/**
	 * Method that Ningyi will create
	 */
	public void preprocess()
	{

	}
	
	/**
	 * Method Gurpreet/Eddie to create that does mass detection.
	 * Could have versions where pass parameters like massDetect(threshold,???)
	 * @param binary_image with colors set to either 0 or 255.
	 * @return ImageData with pixels/colors set to labels 0(no mass), 1(first mass), ..., k. 
	 */
	public ImageData massDetect(ImageData binary_image)
	{
		int[] pixels = binary_image.allocPixels();
		binary_image.getPixels(pixels, 0, 0, width, height);

		ArrayDeque<Integer> pixel_indices = new ArrayDeque<Integer>();		
		for(int i=0; i<pixels.length; i++) {
			pixel_indices.add(i);
		}
		
		int label_counter = 0;
		while(!pixel_indices.isEmpty()) {
			int current_pixel_index = pixel_indices.removeFirst();
			if(Util.getRGBavg(pixels[current_pixel_index]) == 255) {
				label_counter++;
				labelPixel(pixels, current_pixel_index, label_counter);
				pixel_indices.remove(current_pixel_index);
				ArrayDeque<Integer> index_of_neighbors = new ArrayDeque<Integer>();
				addRasterNeighbors(current_pixel_index, index_of_neighbors, width, height);

				while(!index_of_neighbors.isEmpty()){
					int neighbor_pixel_index = index_of_neighbors.removeFirst();
					if(Util.getRGBavg(pixels[neighbor_pixel_index]) == 255) {
						labelPixel(pixels, neighbor_pixel_index, label_counter);
						pixel_indices.remove(neighbor_pixel_index);
						addRasterNeighbors(neighbor_pixel_index, index_of_neighbors, width, height);
					}
				}
			}
		}
//		tempLabelImage(pixels);
		
		ImageData result = binary_image.clone();
		result.setPixels(pixels, 0, 0,
				width, height);
		return result;
	}

	private void labelPixel(int[] pixels, int current_pixel_index, int label_counter) {
		pixels[current_pixel_index] = Util.setRGB(label_counter, label_counter, label_counter);
	}

	public static void addRowOfNeighbors(int x, int y, ArrayDeque<Integer> indexOfNeighbors, int width) {
		// x00
		if(x > 0) {
			indexOfNeighbors.add((y)*width + x - 1);
		}

		// 0x0
		indexOfNeighbors.add((y)*width + x);

		// 00x
		if(x < width - 1) {
			indexOfNeighbors.add((y)*width + x + 1);	
		}
	}

	public static void addRasterNeighbors(int pixelIndex, ArrayDeque<Integer> indexOfNeighbors, int width, int height) {
		int y = pixelIndex / width;
		int x = pixelIndex % width;
		// 000
		// 01x
		// 000
		if(x < width - 1){
			indexOfNeighbors.add(y*width + x + 1);
		}
		// 000
		// 010
		// xxx
		if(y < height -1) {
			addRowOfNeighbors(x , y + 1, indexOfNeighbors, width);
		}
	}

	public static void add8Neighbors(int pixelIndex, ArrayDeque<Integer> indexOfNeighbors, int width, int height) {
		int y = pixelIndex / width;
		int x = pixelIndex % width;
		
		if(y > 0) {
			// xxx
			// 010
			// 000
			addRowOfNeighbors(x, y - 1, indexOfNeighbors, width);
		}

		// 000
		// x10
		// 000
		if(x > 0) {
			indexOfNeighbors.add(y*width + x - 1);
		}
		
		// 000
		// 01x
		// xxx
		addRasterNeighbors(pixelIndex, indexOfNeighbors, width, height);
	}
	
	/**
	 * For testing/devel use only.
	 * Currently, only label first 3 rows with labels 1, 2, and 3.
	 * @see {@link ROI#massDetect(ImageData)} 
	 * @param pixels binary image containing 0 and 255
	 */
	private void tempLabelImage(int[] pixels) {
		for(int y = 0; y < height; y++) {
			for(int x = 0; x < width; x++) {
				pixels[y*width + x] = Util.setRGB(0, 0, 0);
			}
		}
		for(int y = 0; y < 3; y++){
			tempLabelRow(y, y+1, pixels);	
		}
	}

	/**
	 * For testing/devel use only.
	 * @param row
	 * @param label
	 * @param pixels
	 */
	private void tempLabelRow(int row, int label, int[] pixels) {
		for(int x = 0; x < width; x++) {
			pixels[row*width + x] = Util.setRGB(label, label, label);
		}
	}
	
	/**
	 * Region growing function on labeled image
	 * @param labeled_image
	 * Todo add the correct code for region growing
	 */
	public ImageData regionGrowing(ImageData labeled_image){
		ImageData input = labeled_image;
		int[] pixels1 = input.allocPixels();
		input.getPixels(pixels1, 0, 0, width, height);
        ImageData result = labeled_image.clone();
	
		return result; 
	}
	
	/**
	 * Scans current binary_image to create a labeled image from it ...
	 * detecting binary blobs.
	 */

	/**
	 * Method that Boni will create to detect blobs from labeled_image.
	 * The value of the label is taken to be the average of the rgb
	 * component pixel values.  For example, a pixel with an rgb
	 * value of (32, 32, 32) results in the label 32.
	 */
	public void findLabeledMasses()
	{
		// We'll use a Map to link the found masses with our
		// newly created Mass objects.
		HashMap<Integer, Mass> foundMasses = new HashMap<Integer, Mass>();

		// Reset our Mass_Vector
		Mass_Vector.clear(); // Boni: Should we do this here???

		// Get pixels
		int[] pixels = labeled_image.allocPixels();
		labeled_image.getPixels(pixels, 0, 0, width, height);

		// Add each found label to founMasses vector
		for (int y = 0; y < height; y++)
		{
			for (int x = 0; x < width; x++)
			{
				int pixel = pixels[y*width + x];
				int label = Util.getRGBavg(pixel);
				if (label > 0)
				{
					Mass mass;

					// Add if this is a new label,
					// otherwise retrieve the Mass object.
					if (!foundMasses.containsKey(label))
					{
						p.print("Found new label: " + label);
						mass = new Mass(this, label);
						mass.detectBoundary();
						foundMasses.put(label, mass);
					}
					else
					{
						mass = foundMasses.get(label);
					}

					// Update the mass bounding box
					mass.updateBoundingBox(x, y);
				}
			}
		}

		// Save pointer to each created Mass object
		p.print("# of found labels: " + foundMasses.size());
		Iterator<Mass> it = foundMasses.values().iterator();
		while (it.hasNext())
		{
			Mass mass = it.next();
			this.addMass(mass);
			//mass.dump_BoundingBox();	// debug
		}
	}

	public String getFilename() {
		return filename;
	}

	public int getHeight() {
		return height;
	}

	public int getOriginal_xpos() {
		return original_xpos;
	}

	public int getOriginal_ypos() {
		return original_ypos;
	}

	public int getWidth() {
		return width;
	}

	public void setBinaryImage(ImageData binary_image) {
		this.binary_image = binary_image;
	}

	public void setLabeledImage(ImageData labeled_image) {
		this.labeled_image = labeled_image;
	}

	/**
	 * 
	 * @return ImageData labeledImage, grown up
	 */
	ImageData regionGrowing() {
		if(Mass_Vector.isEmpty()) {
			findLabeledMasses();
		}
		for(Mass mass : Mass_Vector) {
			float aveGrey = mass.extract_AveGrey();
			grow(mass, aveGrey);
		}
		mergeTouchingMasses();
		return labeled_image;
	}
	
	private Vector<Mass> mergeTouchingMasses() {
		int[] labeledImagePixels = labeled_image.allocPixels();
		labeled_image.getPixels(labeledImagePixels, 0, 0, width, height);
		
		ArrayDeque<Mass> masses = new ArrayDeque<Mass>(Mass_Vector);
		while(!masses.isEmpty()) {
			Mass mass = masses.removeFirst();
			
			for (Point point: mass.Boundary_Pixels) {
				// create 8-neighbor mask
				ArrayDeque<Integer> indexOfNeighbors = new ArrayDeque<Integer>();
				add8Neighbors(pointToPixelIndex(point), indexOfNeighbors , width, height);
				
				for(int pixelIndex : indexOfNeighbors) {
					ArrayDeque<Mass> otherMasses = new ArrayDeque<Mass>(masses);
					while(!otherMasses.isEmpty()) {
						Mass otherMass = otherMasses.removeFirst();
						if(Util.getRGBavg(labeledImagePixels[pixelIndex]) == otherMass.getMassID()) {
							// merge(mass, otherMass):
							// where we find all pixels in labeledImage labeled otherMass.massID, and label them as mass.massID.
							for (int y = 0; y < height; y++)
							{
								for (int x = 0; x < width; x++)
								{
									if(Util.getRGBavg(labeledImagePixels[y*width + x]) == otherMass.getMassID()) {
										labeledImagePixels[y*width + x] = Util.setRGB(mass.getMassID(),mass.getMassID(),mass.getMassID());
										
									}
								}
							}
							masses.remove(otherMass);
							Mass_Vector.remove(otherMass);
						}
					}
				}

			}
		}
		return Mass_Vector;
	}
	
	int pointToPixelIndex(Point point){
		return point.y * width + point.x;
	}

	Point pixelIndexToPoint(int pixelIndex) {
		return new Point(pixelIndex % width, pixelIndex / width);
	}

	/**
	 * 
	 * @param mass
	 * @param aveGrey
	 */
	private Mass grow(Mass mass, float aveGrey) {
		int[] originalImagePixels = original_image.allocPixels();
		original_image.getPixels(originalImagePixels, 0, 0, width, height);

		int[] labeledImagePixels = labeled_image.allocPixels();
		labeled_image.getPixels(labeledImagePixels, 0, 0, width, height);
		
		
		ArrayDeque<Point> boundaryPointQ = new ArrayDeque<Point> (mass.Boundary_Pixels);
		while(!boundaryPointQ.isEmpty()) {
			Point centerPoint = boundaryPointQ.removeFirst();
			ArrayDeque<Integer> indexOfNeighbors = new ArrayDeque<Integer>();
			int pixelIndex = centerPoint.y * width + centerPoint.x;
			add8Neighbors(pixelIndex, indexOfNeighbors, width, height);
			for(int i: indexOfNeighbors) {
				if((labeledImagePixels[i] == 0)
						&& (Util.getRGBavg(originalImagePixels[i]) - mass.extract_AveGrey() < aveGrey)) {
					labeledImagePixels[i] = mass.getMassID();
					boundaryPointQ.push(pixelIndexToPoint(i));
					// this can be a performance bottleneck
					// can probably expose ImageData.pixelData, instead of
					// copying pixels back and forth via get/setPixels.
					labeled_image.setPixels(labeledImagePixels, 0, 0, width, height);
					aveGrey = mass.extract_AveGrey();
				}
			}
		}
		// got new boundaries now, so need to redetect.
		mass.detectBoundary();
		return mass;
	}
}
